/**
 * Lin-Validator v2.1
 * 作者：7七月, ahhh
 */

const validator = require('validator');
const {Op} = require('sequelize');
const {
  get,
  last,
  set,
  cloneDeep
} = require("lodash");
const {
  ParameterException
} = require('./HttpException');
const {
  findMembers
  , isArrayOf
} = require('./util');


class LinValidator {
  constructor() {
    this.data = {};
    this.parsed = {};
    this._default_member_keys = findMembers(this, {
      filter: this._findMembersFilter.bind(this)
    });
  }

  async validateRecordIfExist(vals) {
    const res = await this.Model.findOne({
      where: {
        id: parseInt(vals.path.id)
      }
    });
    if (res === null) throw new Error('不存在的记录id');
  }

  async validateUniqueFields(vals) {
    const model_attributes = this.Model.rawAttributes;
    const field_names = Object.keys(model_attributes);
    //TODO 考虑用or优化 从而只进行一次查询  但要具体怎么得到哪个字段重复是个问题
    for (let field_name of field_names) {
      const field_value = vals.body[field_name];
      if (!field_value) continue; // 如果该字段没有填写value 不检测其unique性 值都没有怎么可能冲突
      const filed_option = model_attributes[field_name];
      if (filed_option.unique) {
        const findObj = {
          where: {
            [field_name]: field_value
          }
        };
        vals.path.id ? findObj.where.id = {[Op.not]: parseInt(vals.path.id)} : null; // 如果是修改记录,则需要查找id不为自身id的记录(一般修改记录时不会变更自己的id,而是改变的其它字段)
        const res = await this.Model.findOne(findObj);
        if (res !== null) throw new Error(`不允许重复的${field_name}值`);
      }
    }
  }

  async validateIsOwn(vals) {
    const res = await this.Model.findOne({
      where: {
        id: parseInt(vals.path.id)
        , UserId: parseInt(vals.userid)
      }
    });
    if(res === null) throw new Error(`不能修改不属于你自己的记录`);
  }

  /**
   * 选择情景
   * 不同情景拥有不同的验证字段和自定义验证方法
   * @param which_scene
   * @return {LinValidator}
   */
  scene(which_scene) {
    this.which_scene = which_scene;
    return this;
  }

  async validate(ctx, alias = {}, return_now = true) {
    this.alias = alias;
    let params = this._assembleAllParams(ctx);
    this.data = cloneDeep(params);
    this.parsed = cloneDeep(params);

    let member_keys;
    this.which_scene ?
      member_keys = this.$scene[this.which_scene] : // ←根据不同情景筛选验证规则字段和自定义方法
      member_keys = this._default_member_keys; // ←没有设置情景 启用所有验证规则字段和自定义方法

    member_keys = new Set(member_keys);
    const errorMsgs = [];
    for (let key of member_keys) {
      const result = await this._check(key, alias);
      if (!result.success) {
        errorMsgs.push(result.msg);
        if (return_now) break;
      }
    }
    if (errorMsgs.length !== 0) {
      throw new ParameterException({msg: errorMsgs});
    }

    ctx.v = this; //TODO
    return this; //TODO
  }

  get(path, parsed = true) {
    if (parsed) {
      const value = get(this.parsed, path, null);
      if (value == null) {
        const keys = path.split('.');
        const key = last(keys);
        return get(this.parsed.default, key); //new Rule('isOptional','msg','defaultValue') 并且注意 只有parsed为true时 才会得到默认值
      }
      return value;
    } else {
      return get(this.data, path);
    }
  }

  _assembleAllParams(ctx) {
    return {
      body: ctx.request.body,
      query: ctx.request.query,
      path: ctx.params,
      header: ctx.request.header,
      ...ctx.decoded
    };
  }

  _findMembersFilter(key) {
    // 这个是用来筛选要验证的字段用的 不需要加入到members
    if (this[key] === '$scene') return false;

    // 自定义验证方法的名字必须长这样↓
    if (/validate([A-Z])\w+/g.test(key)) {
      return true
    }
    // 定义要校验的属性时 必须是Rule的实例
    if (this[key] instanceof Array) {
      this[key].forEach(value => {
        const isRuleType = value instanceof Rule;
        if (!isRuleType) {
          throw new Error('验证数组必须全部为Rule类型')
        }
      });
      return true
    }
    return false
  }

  async _check(key, alias = {}) {
    const isCustomFunc = typeof (this[key]) == 'function' ? true : false;
    let result;
    if (isCustomFunc) {
      try {
        await this[key](this.data);
        result = new RuleResult(true);
      } catch (error) {
        result = new RuleResult(false, error.msg || error.message || '参数错误');
      }

      // 使用validator.js内置的校验规则
    } else {
      // 属性验证,数组,内有一组Rule this.password1=[new Rule(),new Rule(),...]
      const rules = this[key];
      // 别名替换 比如你要校验一个属性叫y,但你已经在validator中给x属性定义了一个校验规则,这个规则和y要创建的一样,那么你可以给x属性取一个别名叫y,这样y就会使用x定义的规则而无需再手动定义一份
      // 多用于一个validator继承自另外一个,虽然父validator有相同规则,但子validator属性名不一样的时候
      key = alias[key] ? alias[key] : key;
      const param = this._findParam(key); // V验证器里 验证字段所填值时 是依次尝试(按那个key)从query->body->path->header里查找

      const ruleField = new RuleField(rules);
      result = ruleField.validate(param.value);

      if (result.pass) {
        // 如果参数路径不存在,往往是因为用户传了空值,而又设置了默认值
        // 即new Rule('isOptional','msg','defaultValue')
        // 这里比较hack,validator.js里可能存在校验一个字段是nullable(这又什么意义呢？)
        // 但即使存在这样的校验,我们下面只是将这个值赋给了this.parsed.default
        // 把nullable赋给this.parsed.default 也无伤大雅
        // 需要注意的是如果参数是optional且填了默认值且前端没有传这个参数,这个我们自己设置的默认值是不会经过后续规则校验的(如果有的话)
        if (param.path.length === 0) {
          set(this.parsed, ['default', key], result.legalValue)
        } else {
          set(this.parsed, param.path, result.legalValue)
        }
      }
    }
    if (!result.pass) {
      const msg = `${isCustomFunc ? '' : key}${result.msg}`;
      return {
        msg: msg,
        success: false
      }
    }
    return {
      msg: 'ok',
      success: true
    }
  }

  // 这就是为什么说 使用该Validator,path、query、headers、body不能有相同名字的校验属性的原因
  _findParam(key) {
    let value;
    value = get(this.data, ['query', key]);
    if (value !== undefined/*←意思是只要前端上传了这个字段,就返回,不论其值是否为''(前端只要填了这个字段,它是不可能为undefined的,数字在前后端传递,传递的都是字符串,json也是字符串)*/) {
      return {
        value,
        path: ['query', key]
      };
    }
    value = get(this.data, ['body', key]);
    if (value !== undefined) {
      return {
        value,
        path: ['body', key]
      }
    }
    value = get(this.data, ['path', key]);
    if (value !== undefined) {
      return {
        value,
        path: ['path', key]
      }
    }
    value = get(this.data, ['header', key]);
    if (value !== undefined) {
      return {
        value,
        path: ['header', key]
      }
    }

    /*↓说明这个字段压根没有上传*/
    return {
      value: null,
      path: []
    }
  }
}


class RuleResult {
  constructor(pass, msg = '') {
    Object.assign(this, {
      pass,
      msg
    })
  }
}

class RuleField {
  constructor(rules) {
    this.rules = rules
  }

  validate(field) {
    // 如果该字段没有从前端提交上来(比值为nullable还严重)
    if (field === null) {
      const allowEmpty = this._allowEmpty();
      const defaultValue = this._hasDefault();
      if (allowEmpty) {
        return new RuleFieldResult(true, '', defaultValue);
      } else {
        return new RuleFieldResult(false, '字段是必填参数');
      }
    }

    const filedResult = new RuleFieldResult(false);
    for (let rule of this.rules) {
      let result = rule.validate(field);
      if (!result.pass) {
        filedResult.msg = result.msg;
        filedResult.legalValue = null;
        // 一旦一条校验规则不通过，则立即终止这个字段的验证
        return filedResult
      }
    }
    return new RuleFieldResult(true, '', this._convert(field))
  }

  _allowEmpty() {
    for (let rule of this.rules) {
      if (rule.name === 'isOptional') {
        return true
      }
    }
    return false
  }

  _hasDefault() {
    for (let rule of this.rules) {
      const defaultValue = rule.params[0]; // 如果validateFunc为isOptional，则这个参数(new Rule所接受的第三个参数)为默认值
      if (rule.name === 'isOptional') {
        return defaultValue
      }
    }
  }

  _convert(value) {
    for (let rule of this.rules) {
      if (rule.name === 'isInt') {
        return parseInt(value)
      }
      if (rule.name === 'isFloat') {
        return parseFloat(value)
      }
      if (rule.name === 'isBoolean') {
        return value ? true : false
      }
    }
    return value
  }
}

class RuleFieldResult extends RuleResult {
  constructor(pass, msg = '', legalValue = null) {
    super(pass, msg);
    this.legalValue = legalValue;
  }
}

class Rule {
  constructor(name, msg, ...params) {
    Object.assign(this, {
      name, // 即是validator.js里支持的Validator的方法名
      msg,
      params
    })
  }

  validate(field) {
    switch (this.name) {
      //isOptional是我们自定义的不是validator里的, 并且它的应尽的作用在之前的代码里已经尽到了 故这里直接放行
      case 'isOptional':
        break;
      case 'isNotAllowed':
        return new RuleResult(false, this.msg || this.message || '参数错误');
      case 'isNumericArray':
        if (!isArrayOf('number')(field)) return new RuleResult(false, this.msg || this.message || '参数错误');
        break;
      case 'isArray':
        if (!isArrayOf()(field)) return new RuleResult(false, this.msg || this.message || '参数错误');
        break;
      default: // ←validator.js的规则 https://github.com/validatorjs/validator.js
        if (!validator[this.name](field + '', ...this.params)) {
          return new RuleResult(false, this.msg || this.message || '参数错误');
        }
    }

    // 说明没有问题 验证通过
    return new RuleResult(true);
  }
}

module.exports = {
  Rule,
  LinValidator
};
